#include <cerrno>
#include <cstring>
#include <iostream>
#include <string>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <imgui.h>
#include <backends/imgui_impl_glfw.h>
#include <backends/imgui_impl_opengl3.h>
#include <matrix.h>

#include "index_buffer.h"
#include "renderer.h"
#include "shader.h"
#include "texture.h"
#include "vertex_array.h"
#include "vertex_buffer.h"
#include "vertex_buffer_layout.h"

void onKey(GLFWwindow *window, int32_t key, int32_t scancode, int32_t action, int32_t mode)
{
    if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
        glfwSetWindowShouldClose(window, GL_TRUE);
    }
}

int32_t main()
{
    if (!glfwInit()) {
        std::cerr << "glfwInit failed";
        return -1;
    }

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(640, 480, "Hello OpenGL", nullptr, nullptr);
    if (window == nullptr) {
        std::cerr << "glfwCreateWindow failed" << std::endl;
        std::cerr << "errno: " << errno << ", means " << strerror(errno) << std::endl;
        glfwTerminate();
        return -1;
    }

    glfwMakeContextCurrent(window);

    if (glewInit() != GLEW_OK) {
        std::cerr << "glewInit failed";
        return -1;
    }

    int32_t width, height;
    glfwGetFramebufferSize(window, &width, &height);
    glViewport(0, 0, width, height);
    glfwSetKeyCallback(window, onKey);

    printf("GL Version: %s\n", glGetString(GL_VERSION));
    {
        float positions[] = {
            -50.0f, -50.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // 0
             50.0f, -50.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 1
             50.0f,  50.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // 2
            -50.0f,  50.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 3
        };

        uint32_t indices[] = {
            0, 1, 2,
            2, 3, 0,
        };

        GLCALL(glEnable(GL_BLEND));
        GLCALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); // transparent

        VertexArray va;
        VertexBuffer vb(positions, sizeof(positions));

        VertexBufferLayout layout;
        layout.Push<float>(3);
        layout.Push<float>(3);
        layout.Push<float>(2);
        va.AddBuffer(vb, layout);

        IndexBuffer ib(indices, sizeof(indices) / sizeof(*indices));

        glm::mat4 projection = glm::ortho(0.0f, 960.0f, 0.0f, 540.0f, -1.0f, 1.0f);
        glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0));

        Shader shader("out/basic.shader");
        shader.Bind();

        Texture texture("out/qq_head.png");
        texture.Bind(0);
        shader.SetUniform1i("u_Texture", 0);
        texture.Bind(1);
        shader.SetUniform1i("u_Texture2", 1);

        va.Unbind();
        vb.Unbind();
        ib.Unbind();
        shader.Unbind();

        Renderer renderer;
        ImGui::CreateContext();
        ImGui_ImplGlfw_InitForOpenGL(window, true);
        ImGui_ImplOpenGL3_Init();
        ImGui::StyleColorsDark();

        glm::vec3 translationA(200, 200, 0);
        glm::vec3 translationB(400, 200, 0);

        while (glfwWindowShouldClose(window) == 0) {
            renderer.Clear();

            ImGui_ImplOpenGL3_NewFrame();
            ImGui_ImplGlfw_NewFrame();
            ImGui::NewFrame();

            {
                auto model = Matrix<GLfloat>::TranslateMatrix(200, 200);
                model = model * Matrix<GLfloat>::RotateMatrixZ(glfwGetTime());

                shader.Bind();
                shader.SetUniformMat4f("u_ViewProjection", projection * view);
                shader.SetUniformMat4f("u_Model", model);

                renderer.Draw(va, ib, shader);
            }

            {
                auto model = Matrix<GLfloat>::TranslateMatrix(400, 200);

                shader.Bind();
                shader.SetUniformMat4f("u_ViewProjection", projection * view);
                shader.SetUniformMat4f("u_Model", model);

                renderer.Draw(va, ib, shader);
            }

            {
                ImGui::SliderFloat3("translationA", &translationA.x, 0.0f, 960.0f);
                ImGui::SliderFloat3("translationB", &translationB.x, 0.0f, 960.0f);
                ImGui::Text("Application average %.3f ms/frame (%.1f FPS)",
                    1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
            }

            ImGui::Render();
            ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

            glfwSwapBuffers(window);

            glfwPollEvents();
        }
    }

    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
}
